""" S3D.ElectricCraft.Editor module """

import time
from PyQt4.QtCore import QObject, SIGNAL
from PyQt4.QtGui import QDialog
from S3D import range_2
from S3D.ElectricCraft import ViewModel as game
from S3D.ElectricCraft.ViewModel import ViewItem, ViewModel
from S3D.ElectricCraft.ElectricModel import buildElectricModel
from S3D.ElectricCraft.ChipSaveLoadDialog import ChipSaveLoadDialog

STATE_DEFAULT       = 0
STATE_SELECT        = 1
STATE_FILL_ITEMS    = 2

CHIP_BOARD_SIZE     = 20, 20
CHIP_MAX_INPUTS     = 8

CONSTRAINT_COMBINATIONS = [
    (False, False, False, False), # free
    (True,  False, True,  False), # up-down
    (False, True,  False, True ), # left-right

    (True,  False, False, False), # left block
    (False, True,  False, False), # up block
    (False, False, True,  False), # right block
    (False, False, False, True ), # down block

    (True,  False, False, True ), # right-up
    (False, False, True,  True ), # left-up
    (True,  True,  False, False), # right-down
    (False, True,  True,  False), # left-down
]

EDITOR_EVENT_L_DOWN         = 0
EDITOR_EVENT_L_UP           = 1
EDITOR_EVENT_R_DOWN         = 2
EDITOR_EVENT_R_UP           = 3
EDITOR_EVENT_DOUBLE_CLICK   = 4
EDITOR_EVENT_MOVE           = 5
EDITOR_EVENT_KEY            = 6

COMPLEX_ITEM_PARTS          = game.CHIP_PARTS + game.INPUT_OUTPUT_PARTS

class EditorEvent:
    __slots__ = ("type", "pos", "key", "control", "shift")

    def __init__(self, type, **kw):
        self.type = type
        for k, v in kw.items():
            setattr(self, k, v)

    def reset(self):
        self.type   = None
        self.pos    = None
        self.key    = None
        self.control = None
        self.shift = None

class EditorSaveData:
    pass

class Editor(QObject):
    def __init__(self, mainWindow):
        super(Editor, self).__init__()

        self.mainWindow = mainWindow

        self.renderer   = mainWindow.renderer

        self.highlightState = False
        self.highlightPos   = (0, 0)

        self.pushedStates   = []
        self.state = STATE_DEFAULT

        # create main board
        self.viewModel  = ViewModel("main", CHIP_MAX_INPUTS, *CHIP_BOARD_SIZE)
        self.modelStack     = [self.viewModel]
        self.renderer.pushModel(self.viewModel)
        self.renderer.setTitle(self.viewModel.name)

        self.initSelectModels()

        # chip controls
        self.isStarted = False
        self.isRunning = False
        self.lastUpdate = time.clock() # update each 30 ms
        self.inputs = [] # user inputs testing

    def initSelectModels(self):
        # New elements
        elements = [
            game.VIEW_ITEM_POWER,
            game.VIEW_ITEM_INPUT,
            game.VIEW_ITEM_OUTPUT,
            game.VIEW_ITEM_CHIP,
        ]

        elements = [dict(type = p, constr = game.FULL_CONSTR) for p in elements]

        keyStates = [(0, False), (0, True), (1, False), (1, True)]
        keyStates = [dict(type = game.VIEW_ITEM_KEY, direction = d, state = s) for d, s in keyStates]
        elements.extend(keyStates)

        diodeDirections = [dict(type = game.VIEW_ITEM_DIODE, direction = d) for d in range(game.DIRS_COUNT)]
        elements.extend(diodeDirections)
        
        self.newElementModel = self.createSelectModel(elements, 4)

        # Wire constraints
        wireModes = [dict(type = game.VIEW_ITEM_BRIDGE)]
        for constr in CONSTRAINT_COMBINATIONS:
            wireModes.append(dict(type = game.VIEW_ITEM_WIRE, constr = constr))

        elements.extend(wireModes)
        self.changeWireModel = self.createSelectModel(elements, 4)

    def createSelectModel(self, elements, width = None):
        if width is None:
            width = len(elements)
            
        sizeX = width
        sizeY = (len(elements) + width - 1) //  width
        
        res = ViewModel("preview", CHIP_MAX_INPUTS, sizeX, sizeY)
        for x in range(sizeX):
            for y in range(sizeY):
                index = x + y * sizeX
                if index >= len(elements):
                    continue

                if elements[index] is not None:
                    res.setItem(x, y, ViewItem(**elements[index]))

        res.isPreview = True
        return res

    #
    # Event handlers
    #
    def onMouseMove(self, pos):
        if pos is None:
            if self.highlightState:
                self.highlightState = False
                self.emit(SIGNAL("ResetHighlight()"))
            return

        # filter move events
        if pos != self.highlightPos:
            self.event = EditorEvent(EDITOR_EVENT_MOVE, pos = pos)
            self.updateAuto()

        if not self.highlightState or pos != self.highlightPos:
            self.highlightState = True
            self.highlightPos   = pos
            self.emit(SIGNAL("SetHighlight(int, int)"), *pos)

            item = self.getItem(pos)
            if item is not None and item.type in game.CHIP_PARTS:
                self.updateModelTitle(item.chip.model)
            else:
                self.updateModelTitle()

    def onMouseLeftPress(self, pos, control = False, shift = False):
        self.event = EditorEvent(EDITOR_EVENT_L_DOWN,
                                 pos = pos, control = control, shift = shift)
        self.updateAuto()

    def onMouseLeftRelease(self, pos):
        self.event = EditorEvent(EDITOR_EVENT_L_UP, pos = pos)
        self.updateAuto()

    def onMouseRightPress(self, pos, control = False, shift = False):
        self.event = EditorEvent(EDITOR_EVENT_R_DOWN,
                                 pos = pos, control = control, shift = shift)
        self.updateAuto()

    def onMouseRightRelease(self, pos):
        self.event = EditorEvent(EDITOR_EVENT_R_UP, pos = pos)
        self.updateAuto()

    def onMouseDoubleClick(self, pos, control = False, shift = False):
        self.event = EditorEvent(EDITOR_EVENT_DOUBLE_CLICK,
                                 pos = pos, control = control, shift = shift)
        self.updateAuto()
        
    def onKeyPress(self, key, control, shift):
        self.event = EditorEvent(EDITOR_EVENT_KEY, key = key, control = control, shift = shift)
        self.updateAuto()

        # save/load
        # Q - quick save, Ctrl+Q - quick load
        if key == 'q':
            if control:
                self.saveModel(r"saves\q.sav")
            else:
                self.loadModel(r"saves\q.sav")

        # Ctrl+N - new board
        if key == 'n' and control:
            print("new model")
            self.loadModel(createNew = True)

        if key == 's' and not control or key == 'g':
            if not self.isStarted:
                self.electricModel = buildElectricModel(self.viewModel)
                self.inputs = [0 for _ in self.electricModel.inputs]
                self.lastOutputState = [False for _ in self.electricModel.outputs]
                self.isStarted = True
                self.isRunning = True
                self.steps = 0
                print("--- run (%d groups)" % self.electricModel.getGroupsCount())
            else:
                print(self.isRunning and "not running" or "running") 
                self.isRunning = not self.isRunning

        if key == 'f':
            if not self.isStarted:
                self.electricModel = buildElectricModel(self.viewModel)
                self.inputs = [0 for _ in self.electricModel.inputs]
                self.lastOutputState = [False for _ in self.electricModel.outputs]
                self.isStarted = True
                self.isRunning = False
                self.steps = 0
                print("--- start (%d groups)" % self.electricModel.getGroupsCount())
            else:
                self.steps += 1
                print("--- step", self.steps)
                self.electricModel.update()

                for index, p in enumerate(self.electricModel.outputs):
                    if p and p.getUserState() != self.lastOutputState[p.index]:
                        self.lastOutputState[p.index] = p.getUserState()
                        print("#%d Output[%d]: %d" % (self.steps, p.index + 1, p.getUserState() and 100 or 0))

        # compile model (just compilation testing)
        if key == ' ' and not self.isStarted:
            model = buildElectricModel(self.viewModel)
            print("--- groups:", model.getGroupsCount())

        # reset model
        if key == 'r':
            print("reset model")
            self.isStarted = False
            self.isRunning = False
            self.inputs = []
            self.electricModel = None

            ChipSaveLoadDialog.resetModel(self.modelStack[0])
            self.viewModel.update()

        # save/load
        if key == 's' and control:
            self.saveModel()

        if key == 'l' and control:
            self.loadModel()

        # turn on/off groups
        try:
            index = "1234567890".index(key)
        except ValueError:
            index = -1

        if index != -1 and index < len(self.inputs) and self.isStarted:
            next = 1
            if not self.isRunning:
                next = 1 - self.inputs[index]

            self.electricModel.setInputState(index, next)
            self.inputs[index] = next
            print("#%d Input[%d]: %d" % (self.steps, index + 1, next and 100 or 0))

    def onKeyRelease(self, key):
        if not self.isStarted or not self.isRunning:
            return

        try:
            index = "1234567890".index(key)
        except ValueError:
            index = -1

        if index != -1 and index < len(self.inputs):
            self.electricModel.setInputState(index, 0)
            self.inputs[index] = 0
            print("#%d Input[%d]: %d" % (self.steps, index + 1, 0))

    #
    # Process frame
    #
    def update(self):
        if not self.isStarted or not self.isRunning:
            return

        clock = time.clock()
        if clock < self.lastUpdate + 0.03: # each 30 ms
            return

        self.lastUpdate = clock

        self.steps += 1
        self.electricModel.update()

        for index, p in enumerate(self.electricModel.outputs):
            if p and p.getUserState() != self.lastOutputState[p.index]:
                self.lastOutputState[p.index] = p.getUserState()
                print("#%d Output[%d]: %d" % (self.steps, p.index + 1, p.getUserState() and 100 or 0))

    #
    # UI messages
    #
    def getNewChip(self):
        dlg = ChipSaveLoadDialog(self.mainWindow, False)
        if dlg.exec_() != QDialog.Accepted:
            return None

        return dlg.info

    #
    # Misc methods
    #
    def saveModel(self, name = None):
        if name is not None: # old methods for debug
            ChipSaveLoadDialog.saveViewModel(name, self.modelStack[0])
            return

        dlg = ChipSaveLoadDialog(self.mainWindow, True, self.modelStack[-1])
        if dlg.exec_() != QDialog.Accepted:
            return

    def loadModel(self, name = None, createNew = False):
        if name is not None: # old method for debug
            print("load %s" % name)

            res = ChipSaveLoadDialog.loadViewModel(name)
            if res is None:
                print("failed to load")
                return
        else:
            res = None
            if not createNew:
                dlg = ChipSaveLoadDialog(self.mainWindow, False, isMainBoard = True)
                if dlg.exec_() != QDialog.Accepted:
                    return

                res, _ = dlg.info

            if res is None:
                res = ViewModel("main", CHIP_MAX_INPUTS, *CHIP_BOARD_SIZE)

        res.maxInputs = CHIP_MAX_INPUTS # this is a main board, override max inputs

        self.isStarted = False
        self.isRunning = False
        self.inputs = []

        self.modelStack = [res]

        self.renderer.popModel()
        self.renderer.pushModel(res)
        self.viewModel = res
        self.updateModelTitle()

    def updateModelTitle(self, model = None):
        names = [p.name for p in self.modelStack]
        if model is not None:
            names.append(model.name)

        self.renderer.setTitle("/".join(names))

    def processStartSelect(self, pos, model):
        x, y = pos
        mx = x - (model.sizeX - 1) // 2
        my = y - (model.sizeY - 1) // 2
        self.renderer.pushModel(model, mx, my, True)
        self.selectModel = model
        self.selectPos   = pos
        self.state = STATE_SELECT

    def processFinishSelect(self, pos):
        if pos is None:
            item = None
        else:
            x, y = pos
            item = self.selectModel.items[x][y]

        if item is None:
            self.renderer.popModel()
            self.state = STATE_DEFAULT
            return

        x, y = self.selectPos

        if item.type == game.VIEW_ITEM_INPUT:
            if x == 0:
                x += 1 # mark position
            self.removeItem(x - 1, y)
            self.removeItem(x, y)
            input = item.copy()
            self.viewModel.setItem(x, y, input)
            self.viewModel.setItem(x - 1, y, game.ViewItem(game.VIEW_ITEM_MARK, parent = input))
        elif item.type == game.VIEW_ITEM_OUTPUT:
            if x + 1 == self.viewModel.sizeX:
                x -= 1 # mark position
            self.removeItem(x + 1, y)
            self.removeItem(x, y)
            output = item.copy()
            self.viewModel.setItem(x, y, output)
            self.viewModel.setItem(x + 1, y, game.ViewItem(game.VIEW_ITEM_MARK, parent = output))
        elif item.type == game.VIEW_ITEM_CHIP:
            self.putChipAt(x, y)
        else:
            self.removeItem(x, y)
            self.viewModel.setItem(x, y, item.copy())

        self.renderer.popModel()
        self.state = STATE_DEFAULT

    def putChipAt(self, x, y):
        info = self.getNewChip()
        if info is None:
            return

        model, jumpers = info

        x = min(x, self.viewModel.sizeX - 2) # right border, 2 cells needed for inputs, body and outputs
        y = min(y, self.viewModel.sizeY - jumpers)
        chip = ViewItem(game.VIEW_ITEM_CHIP, jumpers = jumpers, model = model)
        for partIndex, index in range_2(2, jumpers):
            if partIndex == 0: # chip inputs
                self.removeItem(x + partIndex, y + index)
                self.viewModel.setItem(x + partIndex, y + index,
                    ViewItem(game.VIEW_ITEM_CHIP_INPUT, index = index, chip = chip))
            else: # chip outputs
                self.removeItem(x + partIndex, y + index)
                self.viewModel.setItem(x + partIndex, y + index,
                    ViewItem(game.VIEW_ITEM_CHIP_OUTPUT, index = index, chip = chip))

    def pushState(self, nextState):
        self.pushedStates.append(self.state)
        self.state = nextState

    def popState(self):
        self.state = self.pushedStates[-1]
        self.pushedStates.pop()

    def getItem(self, pos):
        if pos is None:
            return None
        x, y = pos
        return self.viewModel.items[x][y]

    #
    # Automaton methods
    #
    def updateAuto(self):
        if self.state == STATE_DEFAULT:
            self.updateDrawWireState()
        elif self.state == STATE_FILL_ITEMS:
            self.updateFillItemsState()
        elif self.state == STATE_SELECT:
            self.updateSelectState()

        self.event = None # release event

    def updateDrawWireState(self):
        if self.event.type == EDITOR_EVENT_L_DOWN:
            pos = self.event.pos
            if pos is None:
                return

            x, y = pos
            if not self.event.control and not self.event.shift:
                if self.viewModel.items[x][y] is None:
                    constr = [True, True, True, True]
                    for dir, (dx, dy) in enumerate(game.DIR_LIST):
                        if x + dx not in range(self.viewModel.sizeX) or y + dy not in range(self.viewModel.sizeY):
                            continue

                        next = self.viewModel.items[x + dx][y + dy]
                        if next is not None and not next.constr[game.getRevertedDir(dir)]:
                            constr[dir] = False

                    self.viewModel.setItem(x, y, ViewItem(game.VIEW_ITEM_WIRE, constr = tuple(constr)))
                    self.isMadeWire = True
                else:
                    self.isMadeWire = False

                self.prevPos     = pos
                self.isMakeItems = True
                self.pushState(STATE_FILL_ITEMS)
            elif self.event.control:
                self.removeItem(x, y)
                self.isMakeItems = False
                self.isMadeWire = False
                self.pushState(STATE_FILL_ITEMS)

            return

        if self.event.type == EDITOR_EVENT_R_UP:
            pos = self.event.pos
            if pos is None:
                return

            item = self.getItem(pos)

            if item is None:
                self.processStartSelect(pos, self.newElementModel)
            elif item.type == game.VIEW_ITEM_WIRE or item.type == game.VIEW_ITEM_BRIDGE:
                self.processStartSelect(pos, self.changeWireModel)
            else:
                self.processStartSelect(pos, self.newElementModel)
            return

        if self.event.type == EDITOR_EVENT_DOUBLE_CLICK:
            pos = self.event.pos
            item = self.getItem(pos)
            if item is None:
                return

            if item.type in game.CHIP_PARTS:
                # open child chip
                chip = item.chip
                if chip.model is None:
                    chip.model = ViewModel("untitiled", chip.jumpers, *CHIP_BOARD_SIZE)

                self.renderer.popModel()
                self.renderer.pushModel(chip.model)
                self.modelStack.append(chip.model)
                self.viewModel = chip.model
                self.updateModelTitle()

        if self.event.type == EDITOR_EVENT_KEY:
            if self.event.key == 'esc' and len(self.modelStack) > 1:
                self.modelStack.pop()
                self.viewModel = self.modelStack[-1]

                self.renderer.popModel()
                self.renderer.pushModel(self.viewModel)
                self.updateModelTitle()

    def updateFillItemsState(self):
        if self.event.type == EDITOR_EVENT_L_UP:
            pos = self.event.pos
            if pos is not None:
                x, y = pos
                item = self.viewModel.items[x][y]
                if not self.isMadeWire and item is not None and item.type not in COMPLEX_ITEM_PARTS:
                    self.viewModel.setItem(x, y, None)

            del self.isMadeWire
            del self.isMakeItems
            self.popState()
            return

        if self.event.type == EDITOR_EVENT_MOVE:
            x, y = self.event.pos
            item = self.viewModel.items[x][y]
            
            if not self.isMakeItems:
                if item is not None:
                    self.removeItem(x, y)
                return

            prevX, prevY = self.prevPos
            dirOffset = x - prevX, y - prevY
            if dirOffset not in game.DIR_LIST:
                dir = -1
            else:
                dir = game.DIR_LIST.index(dirOffset)

            if item is None:
                constr = None
                if dir != -1:
                    constr = game.DIODE_CONSTR[dir]

                    prevItem = self.viewModel.items[prevX][prevY]
                    if prevItem is not None and prevItem.type == game.VIEW_ITEM_WIRE:
                        prevItem.resetConstr(dir)

                self.viewModel.setItem(x, y, ViewItem(game.VIEW_ITEM_WIRE, constr = constr))
                self.isMadeWire = True
            else:
                # update current item constraint
                if item.type == game.VIEW_ITEM_WIRE:
                    self.isMadeWire = True
                    # check cross: replace by bridge
                    if item.constr == game.DIODE_CONSTR[game.getRotatedDir(dir)]:
                        self.viewModel.setItem(x, y, ViewItem(game.VIEW_ITEM_BRIDGE))
                    else:
                        item.resetConstr(dir, True)

                # update previous item constraint
                prevItem = self.viewModel.items[prevX][prevY]
                if prevItem.type == game.VIEW_ITEM_WIRE:
                    prevItem.resetConstr(dir)
                    self.isMadeWire = True

                self.viewModel.update()

            self.prevPos = self.event.pos

    def removeItem(self, x, y):
        item = self.viewModel.items[x][y]
        if item is None:
            return

        if item.type not in COMPLEX_ITEM_PARTS:
            self.viewModel.setItem(x, y, None)
            return

        if item.type == game.VIEW_ITEM_INPUT:
            self.viewModel.setItem(x, y, None)
            left = self.viewModel.items[x - 1][y] if x > 0 else None
            if left is not None and left.type == game.VIEW_ITEM_MARK:
                self.viewModel.setItem(x - 1, y, None)
            return
        
        if item.type == game.VIEW_ITEM_OUTPUT:
            self.viewModel.setItem(x, y, None)
            right = self.viewModel.items[x + 1][y] if x + 1 < self.viewModel.sizeX else None
            if right is not None and right.type == game.VIEW_ITEM_MARK:
                self.viewModel.setItem(x + 1, y, None)
            return

        if item.type == game.VIEW_ITEM_MARK:
            parent = item.parent
            if parent.type == game.VIEW_ITEM_INPUT:
                self.removeItem(x + 1, y)
            else:
                self.removeItem(x - 1, y)
            return


        if item.type == game.VIEW_ITEM_CHIP_OUTPUT:
            x -= 1
        y -= item.index
        for dx, dy in range_2(2, item.chip.jumpers):
            self.viewModel.setItem(x + dx, y + dy, None)
        
    def updateSelectState(self):
        if self.event.type == EDITOR_EVENT_L_UP:
            self.processFinishSelect(self.event.pos)
            return

        if self.event.type == EDITOR_EVENT_R_UP:
            self.processFinishSelect(None)
            return
